from PyQt4.QtGui import *
from PyQt4.QtCore import *

from scripts import filters
from docmanager import DocManager
from boqdoc import FilterModel
from cmdobjects import Event, PropDict, CEnum
from conversions import tidy, untidy


def _addact(widget, str, func):
   '''Utility functions to add an action with the specified name and function to a widget.'''
   act = QAction(str, widget)
   widget.addAction(act)
   widget.connect(act, SIGNAL('triggered()'), func)

# ---------------------------------------------------------------------------
#  Gui Manager
# ---------------------------------------------------------------------------

class Manager(object):
   '''This manager contains all the gui objects including the main window.'''
   __instance = None

   def __new__(cls):
      '''Implements a singleton pattern.'''
      if cls.__instance is None:
         self = cls.__instance = object.__new__(cls)
         
         #------------------------------------
         # Set up widgets
         
         # Main window
         self.mainwindow = MainWindow()
         
         # Table views
         self.schedules = View('Schedules', DockWidget.dockLeft)
         self.sections  = View('Sections', DockWidget.dockLeft)
         self.items     = View('Items', DockWidget.dockCentre)
         self.attributes = AttributeView(DockWidget.dockRight)
         
         self.views = self.schedules, self.sections, self.items
         
         #------------------------------------
         # Define events

         self.evt_overlay_master = Event()
         self.evt_currentview = Event()

         #------------------------------------
         # Bind events
         
         # Activate/deactivate master overlay
         self.evt_overlay_master.bind(self.startOverlay, self.stopOverlay)
         
         for i in self.views:
            # Link the selected item to display in the properties window
            i.evt_select.bind(self.properties, self.properties)
            # Keep track of whether a view is selected
            i.widget().evt_focus.bind(self._focuschange, self._focuschange)
         
         # Activate/deactivate GUI when a file is opened/closed
         DocManager().evt_opened.bind(self.openGUI, self.closeGUI)
         
      return cls.__instance

   def initialise(self):
      #------------------------------------
      # Set up menus
      import cui
      for name, menus in cui.menulayout.iteritems():
         for menu in menus:
            getattr(self, name).addMenu(menu, cui.menus[menu])
      del cui
      

   def createMenu(self, item, parent=None):
      '''Create a menu. If the item is a tuple, it is a menu, else it is a menu item. '''
      if isinstance(item, tuple):
         menu = parent.addMenu(item[0])
         for act in item[1]:
            it = self.createMenu(act, menu)
         return menu
         
      elif item is None:
         return parent.addSeparator()
         
      else:
         return parent.addAction(item(parent).action)

   def _focuschange(self):
      if self.currentView() is None:
         (~self.evt_currentview)()
      else:
         self.evt_currentview()
      
      
   def refresh(self):
      for widget in self.mainwindow.children():
         if hasattr(widget, 'refresh'):
            widget.refresh()

   # Return the currently selected objects
   
   def currentView(self):
      return qApp.focusWidget() if isinstance(qApp.focusWidget(), TableView) else None
      
   def currentModel(self):
      return self.currentView().model() if self.currentView() is not None else None
      
   def currentObject(self, aspect):
      return self.currentView().currentObject(aspect) if self.currentView() is not None else None
      
   def currentColumn(self):
      return self.currentView().currentColumn() if self.currentView() is not None else None

   def currentRow(self):
      return self.currentView().currentRow() if self.currentView() is not None else None
      
   def currentCell(self):
      return self.currentView().currentCell() if self.currentView() is not None else None
      
   def currentSchedule(self):
      return self.schedules.currentObject('row') if self.schedules is not None else None
      
   def scheduleView(self):
      return self.schedules.widget()

   def sectionView(self):
      return self.sections.widget()
      
   def itemView(self):
      return self.items.widget()


   # Action/Event targets
   
   def startOverlay(self):
      self.selectSchedule(self)
      
   def stopOverlay(self):
      self.selectSchedule(self)
      
   def properties(self):
      objects = dict([(tidy(a), self.currentObject(a)) for a in ['cell', 'column', 'row']])
      self.attributes.setObjects(objects)
      
   def openGUI(self):
      '''Called when a new document is opened to initialise the gui.'''
      self.schedules.setModel(DocManager().doc.schedules)
      self.schedules.evt_select.bind(self.selectSchedule, self.deselectSchedule)

   def closeGUI(self):
      '''Called when a document is closed to clear the gui.'''
      self.schedules.setModel(None)

   def selectSchedule(self):
      model = self.currentSchedule().sections
      if not self.evt_overlay_master:
         model = FilterModel(model, filters.isactive)
      self.sections.setModel(model)
      self.sections.evt_select.bind(self.selectSection, self.deselectSection)

   def deselectSchedule(self):
      self.deselectSection()
      self.sections.setModel(None)
      
   def selectSection(self):
      m = FilterModel(self.currentSchedule().items, filters.insection(self.currentObject('row')))
      self.items.setModel(m)

   def deselectSection(self):
      self.items.setModel(None)

# ---------------------------------------------------------------------------
#  Widgets
# ---------------------------------------------------------------------------

class MainWindow(QMainWindow):

   def __init__(self, parent=None):
      QMainWindow.__init__(self, parent)

      mw = QMainWindow
      self.dockOptions = mw.AnimatedDocks | mw.AllowNestedDocks | mw.AllowTabbedDocks

      # Create central widget
      self.setCentralWidget(QTabWidget())
      self.centralWidget().setTabPosition(QTabWidget.South)
      
      self.statusBar().showMessage('')
      self.setWindowTitle('boq')
      self.resize(800, 600)
      

   def addMenu(self, name, menu):
      Manager().createMenu((name, menu), self.menuBar())

# ---------------------------------------------------------------------------
#  Views
# ---------------------------------------------------------------------------

class DockWidget(QDockWidget):
   dockLeft = Qt.LeftDockWidgetArea
   dockRight = Qt.RightDockWidgetArea
   dockTop = Qt.TopDockWidgetArea
   dockBottom = Qt.BottomDockWidgetArea
   dockCentre = -1

   def __init__(self, name, table, area):
      '''Create a new dock widget with the following properties:
      area     One of Qt.DockWidgetArea or -1, indicating that it should be
               placed in the centre.
      parent   The parent window.
      '''
      # Set up widgets
      QDockWidget.__init__(self, name, Manager().mainwindow)
      self.setWidget(table)
      self.setAllowedAreas(Qt.AllDockWidgetAreas)
      
      #The following event occurs when an item in the table displayed in the dock is selected
      self.evt_select = Event()

      # Use tabs instead of a dock if area is -1, and place it in the centre.
      if area == -1:
         #self.setFeatures(QDockWidget.NoDockWidgetFeatures)
         self.parent().centralWidget().addTab(self, name)

      # Assign dock widget to area
      elif isinstance(area, QDockWidget):
         self.parent().tabifyDockWidget(area, self)
      else:
         self.parent().addDockWidget(area, self)

      self.show()

   def addMenu(self, name, menu):
      for action in menu:
         Manager().createMenu(action, self.widget())

   def setModel(self, model):
      self.widget().setModel(model)
      self.connect(self.widget().selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self._active_changed)
      
   def _active_changed(self, current, previous):
      if current.isValid():
         self.evt_select()
      else:
         (~self.evt_select)()

   def refresh(self):
      self.widget().model().reset()

   
class View(DockWidget):

   def __init__(self, name, area, filter=filters.all):
      '''Create a new dock widget with the following properties:
      area     One of Qt.DockWidgetArea or -1, indicating that it should be
               placed in the centre.
      parent   The parent window.
      '''
      DockWidget.__init__(self, name, TableView(), area)

   currentObject = lambda self, aspect: self.widget().currentObject(aspect)
   currentRow    = lambda self: self.widget().currentRow()
   currentColumn = lambda self: self.widget().currentColumn()
   currentCell   = lambda self: self.widget().currentCell()


# ---------------------------------------------------------------------------
#  Item Views
# ---------------------------------------------------------------------------

class ColumnHeader(QHeaderView):
   def __init__(self, *args):
      QHeaderView.__init__(self, *args)
      self.setContextMenuPolicy(Qt.ActionsContextMenu)
      
      
class TableView(QTableView):
   '''An editable table view.'''

   def __init__(self):
      QTableView.__init__(self)
      self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
      self.setHorizontalHeader(ColumnHeader(Qt.Horizontal, self))
      self.horizontalHeader().setStretchLastSection(True)
      self.setContextMenuPolicy(Qt.ActionsContextMenu)

      _addact(self, 'Insert row', self.insertRow)
      _addact(self, 'Delete row', self.deleteRow)
      _addact(self, 'Insert column', self.insertColumn)
      _addact(self, 'Delete column', self.deleteColumn)
      
      self.evt_focus = Event()
      
   def focusInEvent(self, event):
      self.evt_focus()
      QTableView.focusInEvent(self, event)
      
   def focusOutEvent(self, event):
      (~self.evt_focus)()
      QTableView.focusOutEvent(self, event)
   

   def currentObject(self, aspect):
      index = { 'row': self.currentRow(), 
                'column': self.currentColumn(), 
                'cell': self.currentCell() }[aspect]
      if index is None: return None 
      index = { 'row': (index, None),
                'column': (None, index),
                'cell': index }[aspect]
      return self.model()[index]
      
   def currentRow(self):
      '''Returns the currently selected item.'''
      return self.currentIndex().row() if self.currentIndex().isValid() else None

   def currentColumn(self):
      '''Returns the currently selected item.'''
      return self.currentIndex().column() if self.currentIndex().isValid() else None

   def currentCell(self):
      i = (self.currentRow(), self.currentColumn())
      return None if None in i else i

   def insertRow(self):
      if self.model() is not None: self.model().insertRow(self.currentRow())
   def deleteRow(self): 
      if self.model() is not None: self.model().deleteRow(self.currentRow())
   def insertColumn(self): 
      if self.model() is not None: self.model().insertColumn(self.currentColumn())
   def insertColumnWithArgs(self, **kwargs):
      if self.model() is not None: self.model().insertColumnWithArgs(self.currentColumn(), **kwargs)
   def deleteColumn(self): 
      if self.model() is not None: self.model().deleteColumn(self.currentColumn())
      
   def setModel(self, m):
      self.__m = m
      return QTableView.setModel(self, m)

# ---------------------------------------------------------------------------
#  Delegates
# ---------------------------------------------------------------------------

class ItemDelegate(QItemDelegate):
   
   def getenumdata(self, index):
      '''Where data(index) is a list [text, [options]]'''
      if index.model().data(index, Qt.EditRole).canConvert(QVariant.List):
         data = index.model().data(index, Qt.EditRole).toList()
         data = [data[0].toString(), [d.toString() for d in data[1].toList()]]
         assert data[0] in data[1], 'Invalid option'
         return data
      
   def createEditor(self, parent, option, index):
      if self.getenumdata(index):
         return QComboBox(parent)
      else:
         return QItemDelegate.createEditor(self, parent, option, index)
         
   def setEditorData(self, editor, index):
      data = self.getenumdata(index)
      if data:
         assert isinstance(editor, QComboBox)
         editor.addItems(data[1])
         editor.setEditText(data[0])            
      else:
         QItemDelegate.setEditorData(self, editor, index)
      
   def setModelData(self, editor, model, index):
      olddata = self.getenumdata(index)
      if olddata:
         assert isinstance(editor, QComboBox)
         data = editor.currentText() if editor.currentText() in olddata[1] else olddata[0]
         model.setData(index, QVariant(data), Qt.EditRole)
      else:
         QItemDelegate.setModelData(self, editor, model, index)
         
   def updateEditorGeometry(self, editor, option, index):
      return QItemDelegate.updateEditorGeometry(self, editor, option, index)


# ---------------------------------------------------------------------------
#  Attribute Model/View
# ---------------------------------------------------------------------------

class AttributeView(DockWidget):
   def __init__(self, area):
      DockWidget.__init__(self, 'Attributes', AttributeTable(), area)

   def setObjects(self, objects):
      self.widget().setModel(AttributeModel(self, objects))
      self.widget().expandAll()

class AttributeTable(QTreeView):

   def __init__(self):
      QTreeView.__init__(self)
      #self.setIndentation(2)
      #self.setRootIsDecorated(False)
      self.setEditTriggers(QAbstractItemView.AllEditTriggers)
      self.setSelectionMode(QAbstractItemView.NoSelection)
      #self.itemsExpandable = True
      self.__delegate = ItemDelegate()
      self.setItemDelegate(self.__delegate)

class AttributeModel(QStandardItemModel):
   '''An attribute editor which reads attributes from a CAttrBase into a two
   column grid for editing.'''

   class GroupItem(QStandardItem):
      def __init__(self, text=''):
         QStandardItem.__init__(self, text)
         self.setBackground(qApp.palette().window())
         self.setEditable(False)
         
   class KeyItem(QStandardItem):
      def __init__(self, base, key):
         QStandardItem.__init__(self, tidy(key))
         self._base = base
         self._key = key
         self.setToolTip(base.properties[key].__doc__)
         self.setEditable(False)
         self.setBackground(qApp.palette().alternateBase())
         self.reloadChildren()

      def reloadChildren(self):
         self.removeRows(0, self.rowCount())
         value = getattr(self._base, self._key)
         if isinstance(value, PropDict):
            AttributeModel.addpropdict(self, value)

      def type(self):
         return QStandardItem.UserType + 1

   class ValueItem(QStandardItem):
      def __init__(self, sibling):
         QStandardItem.__init__(self)
         self._base = sibling._base
         self._key = sibling._key
         self.sibling = sibling
         self.setEditable(not self._base.properties[self._key].const)
         
      def setData(self, value, role):
         if role == Qt.EditRole and not self._base.properties[self._key].const:
            value = str(value.toString())
            if hasattr(self, 'currentoptions'):
               value = self.currentoptions[value]
            setattr(self._base, self._key, value)
            self.sibling.reloadChildren()
            
         else:
            QStandardItem.setData(self, value, role)
            
      def data(self, role):
         if role == Qt.ToolTipRole:
            return QVariant(self._base.properties[self._key].__doc__)

         if role == Qt.EditRole and not self.isEditable():
            return QVariant()

         # Try assigning the value. If the data type is not acceptable, convert it to a string
         value = getattr(self._base, self._key)
         try:
            value = QVariant(value)
         except TypeError:
            value = QVariant(str(value))
            
         if role == Qt.EditRole and isinstance(self._base.properties[self._key], CEnum):
            self.currentoptions = dict([(str(opt), opt) for opt in self._base.properties[self._key].options])
            return QVariant([value, QVariant(self.currentoptions.keys())])
            
         elif role in (Qt.EditRole, Qt.DisplayRole):
            return value
         else:
            return QStandardItem.data(self, role)

      def type(self):
         return QStandardItem.UserType + 2

      
   def __init__(self, parent, bases):
      '''Bases is a dict of objects inherited from PropDict. An entry is created for each 
      based on the key and the properties listed (with children).'''
      QAbstractTableModel.__init__(self, parent)
      for text, base in bases.iteritems():
         parent = AttributeModel.GroupItem(text)
         self.invisibleRootItem().appendRow([parent, AttributeModel.GroupItem()])
         if isinstance(base, PropDict):
            self.addpropdict(parent, base)

   @staticmethod
   def addpropdict(parent, propdict):
      for key in propdict.properties.iterkeys():
         ikey = AttributeModel.KeyItem(propdict, key)
         ivalue = AttributeModel.ValueItem(ikey)
         parent.appendRow([ikey, ivalue])
      
#   def buddy(self, index):
#      return self.index(index.row(), 1)
#
   def headerData(self, section, orientation, role=Qt.DisplayRole):
      if orientation == Qt.Horizontal and role == Qt.DisplayRole:
         return QVariant(('Attribute', 'Value')[section])
      else:
         return QVariant()
         
